package cumeo89.chii.client.entry;

import static com.google.gwt.event.dom.client.KeyCodes.*;

import java.util.ArrayList;

import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.xml.client.Attr;
import com.google.gwt.xml.client.Document;
import com.google.gwt.xml.client.Element;
import com.google.gwt.xml.client.NamedNodeMap;
import com.google.gwt.xml.client.Node;
import com.google.gwt.xml.client.NodeList;

import cumeo89.chii.client.entry.edit.NodeValueChangingCommand;
import cumeo89.chii.client.entry.editor.DefaultEditorFactory;
import cumeo89.chii.client.entry.renderer.DefaultRendererFactory;
import cumeo89.chii.client.ui.ExtendedPanel;
import cumeo89.chii.client.ui.ExtendedScrollPanel;
import cumeo89.chii.client.undo.UndoableEditListener;
import cumeo89.chii.client.undo.UndoableSupport;
import cumeo89.chii.client.util.MyChangeListener;
import cumeo89.chii.client.util.MyChangeListenerCollection;
import cumeo89.chii.client.util.XMLUtil;

public class Entry extends Composite implements EntryModelListener,
		EditorListener {

	private EntryModel model = null;

	private RendererFactory rendererFactory = new DefaultRendererFactory(this);
	private EditorFactory editorFactory = new DefaultEditorFactory();

	private MyChangeListenerCollection listeners = new MyChangeListenerCollection();
	private UndoableSupport undoableSupport = new UndoableSupport();

	private ArrayList<Renderer> renderers = new ArrayList<Renderer>();
	private ExtendedScrollPanel pnlContainer = new ExtendedScrollPanel();
	private Node selected = null;
	private Editor editing = null;
	private Node editingNode = null;

	public Entry() {
		pnlContainer.setStyleName("entry");
		initWidget(pnlContainer);
		sinkEvents(Event.ONKEYUP);
	}

	@Override
	public void onBrowserEvent(Event event) {
		if (event.getTypeInt() == Event.ONKEYUP
				&& (event.getKeyCode() == KEY_UP
						|| event.getKeyCode() == KEY_DOWN
						|| event.getKeyCode() == KEY_LEFT || event.getKeyCode() == KEY_RIGHT)) {
			Node toSelect = getNodeToSelect(event.getKeyCode(), selected);
			if (toSelect != null) {
				setSelected(toSelect);
			}
			event.preventDefault();
			event.stopPropagation();
		}
		super.onBrowserEvent(event);
	}

	private Node getNodeToSelect(int keyCode, Node current) {
		if (current == null) {
			return null;
		}
		Node toSelect = null;
		Node container = XMLUtil.getContainerNode(current);
		Renderer delegate = findRenderer(container);
		if (delegate == null) {
			return null;
		}
		switch (keyCode) {
		case KEY_UP:
			toSelect = delegate.getAboveNode(current);
			break;
		case KEY_DOWN:
			toSelect = delegate.getBottomNode(current);
			break;
		case KEY_RIGHT:
			toSelect = delegate.getRightNode(current);
			break;
		case KEY_LEFT:
			toSelect = delegate.getLeftNode(current);
			break;
		default:
			return null;
		}
		if (toSelect == null) {
			return getNodeToSelect(keyCode, container);
		}

		container = null;
		while (toSelect != container) {
			container = toSelect;
			Renderer renderer = findRenderer(toSelect);
			if (keyCode == KEY_UP || keyCode == KEY_LEFT) {
				toSelect = renderer.getLastNode();
			} else {
				toSelect = renderer.getFirstNode();
			}
		}
		return toSelect;
	}

	/*
	 * Public properties
	 */
	public EntryModel getModel() {
		return model;
	}

	public Document getDocument() {
		return model == null ? null : model.getDocument();
	}

	public Element getRoot() {
		return model == null ? null : model.getDocument().getDocumentElement();
	}

	public void setModel(EntryModel newModel) {
		if (this.model != null) {
			this.model.removeEntryModelListener(this);
		}
		this.model = newModel;
		this.model.addEntryModelListener(this);
		reloadModel();
	}

	public void documentUpdated(EntryModel model) {
		if (this.model != model) {
			return;
		}
		reloadModel();
	}

	private void reloadModel() {
		selected = null;
		editing = null;
		editingNode = null;
		renderers.clear();
		pnlContainer.clear();
		recursiveAddNode(getRoot());
	}

	public Node getSelectedNode() {
		return selected;
	}

	public void setSelected(Node selected) {
		if (this.selected == selected || getRoot().equals(selected)) {
			return;
		}
		Node oldSelected = this.selected;
		this.selected = selected;
		if (editing != null) {
			cancelEdit();
		}
		if (oldSelected != null) {
			nodeUpdated(model, oldSelected, null);
		}
		if (selected != null) {
			nodeUpdated(model, selected, null);
			pnlContainer.ensureVisible(findWidget(selected));
		}
		listeners.fireChange(this);
	}

	public void addChangeListener(MyChangeListener lis) {
		if (!listeners.contains(lis)) {
			listeners.add(lis);
		}
	}

	public void removeMyChangeListener(MyChangeListener lis) {
		listeners.remove(lis);
	}

	public MyChangeListener[] getChagMyChangeListeners() {
		return (MyChangeListener[]) listeners.toArray();
	}

	public void edit(Node node) {
		editing = editorFactory.getEditor(node);
		if (editing == null) {
			return;
		}
		editingNode = node;
		editing.addEditorListener(this);

		final Widget w = findWidget(editingNode);
		editing.startEditing((node.getNodeType() == Node.ELEMENT_NODE ? node
				.getFirstChild() : node).getNodeValue(), w.getAbsoluteLeft(), w
				.getAbsoluteTop(), w.getOffsetWidth(), w.getOffsetHeight());
	}

	public void cancelEdit() {
		if (editing != null) {
			editing.cancelEditing();
		}
	}

	public Node getEditingNode() {
		return editingNode;
	}

	/*
	 * implements EntryModelListener
	 */
	public void nodeInserted(EntryModel model, Node node) {
		recursiveAddNode(node);
	}

	public void nodeUpdated(EntryModel model, Node node, String oldValue) {
		Renderer renderer = findRenderer(node);
		Widget oldWidget = renderer.getWidget();
		renderer.prepareWidget(node, node == selected);
		Widget newWidget = renderer.getWidget();
		if (oldWidget != newWidget) {
			((ExtendedPanel) oldWidget.getParent())
					.insert(newWidget, oldWidget);
			oldWidget.removeFromParent();

			if (oldWidget instanceof HasWidgets) {
				for (Widget child : (HasWidgets) oldWidget) {
					((HasWidgets) newWidget).add(child);
				}
			}
		}
	}

	public void nodeRemoved(EntryModel model, Node node, Node parent,
			Node refChild) {
		recursiveRemoveNode(node);
	}

	public void attrSetted(EntryModel model, Element element, Attr attr,
			String oldValue) {
		findRenderer(element).addRenderer(prepareRenderer(attr));
	}

	public void attrRemoved(EntryModel model, Element element, Attr attr) {
		if (attr.equals(selected)) {
			selected = null;
		}
		findRenderer(element).removeRenderer(findRenderer(attr));
		recursiveRemoveNode(attr);
	}

	private Renderer prepareRenderer(Node node) {
		Renderer renderer = rendererFactory.getRenderer(node);
		if (renderer != null) {
			renderers.add(renderer);
			renderer.prepareWidget(node, node == selected);
		}
		return renderer;
	}

	public void addUndoableEditListener(UndoableEditListener listener) {
		undoableSupport.addUndoableEditListener(listener);
	}

	public void removeUndoableEditListener(UndoableEditListener listener) {
		undoableSupport.removeUndoableEditListener(listener);
	}

	/*
	 * implements EditorListener
	 */
	public void editingSuccessed(Editor editor) {
		if (this.editing != editor) {
			return;
		}
		Node toSelect = this.editingNode;
		Node toChange = editingNode.getNodeType() == Node.ELEMENT_NODE ? editingNode
				.getFirstChild()
				: editingNode;
		undoableSupport.post(this, new NodeValueChangingCommand(model,
				toChange, editor.getEditedValue()));
		clearEditStatus();
		setSelected(toSelect);
	}

	public void editingCancelled(Editor editor) {
		if (editing != editor) {
			return;
		}
		clearEditStatus();
	}

	private void clearEditStatus() {
		this.editingNode = null;
		this.editing.removeEditorListener(this);
		this.editing = null;
	}

	/*
	 * Utilities
	 */
	private void recursiveAddNode(Node node) {
		recursiveAddNode(findRenderer(node.getParentNode()),
				prepareRenderer(node));
	}

	private void recursiveAddNode(Renderer parent, Renderer renderer) {
		if (parent == null) {
			pnlContainer.setWidget(renderer.getWidget());
		} else {
			parent.addRenderer(renderer);
		}

		Node node = renderer.getRenderedNode();
		if (node.getNodeType() == Node.ELEMENT_NODE) {
			NamedNodeMap attrs = node.getAttributes();
			for (int i = 0; i < attrs.getLength(); i++) {
				renderer.addRenderer(prepareRenderer(attrs.item(i)));
			}

			NodeList children = node.getChildNodes();
			for (int i = 0; i < children.getLength(); i++) {
				recursiveAddNode(renderer, prepareRenderer(children.item(i)));
			}
		}
	}

	private void recursiveRemoveNode(Node node) {
		if (node.equals(selected)) {
			selected = null;
		}

		Renderer renderer = findRenderer(node);
		renderer.getWidget().removeFromParent();
		renderers.remove(renderer);

		if (node.getNodeType() == Node.ELEMENT_NODE) {
			NamedNodeMap attributes = node.getAttributes();
			for (int i = 0; i < attributes.getLength(); i++) {
				recursiveRemoveNode(attributes.item(i));
			}

			NodeList children = node.getChildNodes();
			for (int i = 0; i < children.getLength(); i++) {
				if (children.item(i) instanceof Node) {
					recursiveRemoveNode(children.item(i));
				}
			}
		}
	}

	private Widget findWidget(Node node) {
		Renderer renderer = findRenderer(node);
		return renderer == null ? null : renderer.getWidget();
	}

	private Renderer findRenderer(Node node) {
		for (Renderer renderer : renderers) {
			if (renderer.getRenderedNode().equals(node)) {
				return renderer;
			}
		}
		return null;
	}

}